/* eslint-disable @typescript-eslint/no-explicit-any */

import { Component, type CSSStyle, type StyledProps, setCSS } from '@knno/jsx';
import SVGSort from '@mdi/svg/svg/sort.svg';
import SVGArrowUp from '@mdi/svg/svg/sort-ascending.svg';
import SVGArrowDown from '@mdi/svg/svg/sort-descending.svg';
import { vars } from '../theme/vars.css';
import { size2px } from '../tools/utils';
import { MIN_WIDTH } from './gridBaseConfig';
import { Icon } from './icon/icon';
import { Check } from './input/input';
import { DragMask } from './mask/mask';
import { DATA } from './symbol';

export type RowType = Record<string | number, unknown> | any;
export type SortType = 'simple' | 'combine' | 'none';
export type ColumnSizeType = 'scale' | 'fixed' | 'auto';
export type MeasureType =
	| 'resize'
	| 'headline'
	| 'sort'
	| 'col-resize'
	| 'col-fixed'
	| 'columns'
	| 'selection'
	| 'rows'
	| 'current-page'
	| 'refresh';

type Content = Node | string;
export type Contents = Content | Content[];
export function setContent(element: Element, content: Contents): void {
	if (Array.isArray(content)) {
		element.replaceChildren(...content);
	} else {
		element.replaceChildren(content);
	}
}

export type Column = {
	key?: string | number;
	title?: string;
	align?: 'left' | 'center' | 'right';
	width?: number | string;
	fixed?: boolean;
	/**
	 * Whether rows can be sorted by this column
	 */
	sortable?: boolean;
	orderBy?: 'asc' | 'desc' | 'none';
	/**
	 * Whether the column can be resize by user drag
	 */
	resizable?: boolean;
	/**
	 * Style will be apply to every column cells includ header cell
	 */
	style?: CSSStyle;
	/**
	 * Custom cell content
	 * @param value current cell value, if cell is in the head line, value is the column.title
	 * @param row current render line index, -1 is head line
	 * @param col current render column index
	 */
	formatter?: (row: RowType, cell: unknown, rowIndex: number, colIndex: number) => Contents;
	/**
	 * provide custom compare function for sort
	 */
	compare?: (a: unknown, b: unknown) => number;
};

function compare(a: unknown, b: unknown, col: Column): number {
	if (typeof col.compare === 'function') {
		return col.compare(a, b);
	} else {
		if ((a === null || a === undefined) && b !== null && b !== undefined) {
			return -1;
		} else if ((b === null || b === undefined) && a !== null && a !== undefined) {
			return 1;
		}
		if (typeof a === 'number' && typeof b === 'number') {
			return a - b;
		} else {
			return (a + '').localeCompare(b + '');
		}
	}
}

function getValue(row: RowType, col: Column): unknown {
	if (col.key === undefined) {
		return row;
	} else if (row === undefined || row === null) {
		return undefined;
	} else {
		return (row as any)[col.key];
	}
}

export function genWidth(width: number | string | undefined): string {
	if (typeof width === 'number') {
		return `${Math.round(width)}px`;
	} else if (typeof width === 'string') {
		return width;
	} else {
		return '';
	}
}

type CellLike = { rowIdx: number; colIdx: number };

function findCell(element: Element | null): CellLike | null {
	const isCell = (elem: any): elem is CellLike => {
		return elem.rowIdx >= 0 && elem.colIdx >= 0;
	};
	while (element && !isCell(element)) {
		element = element.parentElement;
	}
	return element;
}

export type Section = {
	key: string | number;
	align?: 'left' | 'center' | 'right';
	/**
	 * Style will be apply to every column cells includ header cell
	 */
	style?: CSSStyle;
	/**
	 * Custom cell content
	 * @param value current cell value, if cell is in the head line, value is the column.title
	 * @param row current render line index, -1 is head line
	 */
	formatter?: (value: unknown, row: number) => Contents;
};

export type GridBaseData = {
	data: RowType[];
	fixed: number;
	sorted: RowType[] | null;
	columns: Column[];
	section: Section | null;
	selected: Set<unknown>;
	selectable: boolean;
	actived: number | null;
	checkcol: Column;
	disableField: number | string | null;
};
export type GridRowDragDetail = {
	rowIndex: number;
	row: RowType;
};
export type GridCellClickDetail = {
	colIndex: number;
	rowIndex: number;
	row: RowType;
};
export type GridRowPressDetail = {
	rowIndex: number;
	row: RowType;
	key: string;
	code: string;
};
export type GridReorderDetail = {
	ordered: RowType[];
};
export type RowCheckDetail = {
	row: unknown;
	rowIndex: number;
	checked: boolean;
};
export type RowActiveDetail = {
	row: unknown;
	rowIndex: number;
};

export type GridBaseProps<T extends GridBase<any, any>> = StyledProps<T> & {
	onRowClick?: (this: T, detail: GridCellClickDetail, evt: MouseEvent) => void;
	onRowPress?: (this: T, detail: GridRowPressDetail, evt: KeyboardEvent) => void;
	onRowDblClick?: (this: T, detail: GridCellClickDetail, evt: MouseEvent) => void;
	onRowCheck?: (this: T, event: RowCheckDetail) => void;
	onRowContextMenu?: (this: T, detail: GridCellClickDetail, evt: MouseEvent) => void;
	onRowActive?: (this: T, detail: RowActiveDetail) => void;
	onRowDrag?: (this: T, detail: GridRowDragDetail, evt: DragEvent) => void;
	onRowDragStart?: (this: T, detail: GridRowDragDetail, evt: DragEvent) => void;
	onRowDragEnd?: (this: T, detail: GridRowDragDetail, evt: DragEvent) => void;
	onRowDragOver?: (this: T, detail: GridRowDragDetail, evt: DragEvent) => void;
	onRowDragEnter?: (this: T, detail: GridRowDragDetail, evt: DragEvent) => void;
	onRowDragLeave?: (this: T, detail: GridRowDragDetail, evt: DragEvent) => void;
	onRowDrop?: (this: T, detail: GridRowDragDetail, evt: DragEvent) => void;
	onSelectChange?: (this: T, detail: Set<unknown>) => void;
	onReorder?: (this: T, evt: GridReorderDetail) => void;
	columnBorder?: boolean;
	columnSize?: ColumnSizeType;
	columns?: Column[];
	headLine?: boolean;
	sortType?: SortType;
	selectable?: boolean;
	selected?: RowType[];
	activable?: boolean;
	active?: number | null;
	rows?: RowType[];
	showEmpty?: boolean;
	fixColumns?: number;
	section?: Section | null;
	disableOn?: string | number | null;
	rowDraggable?: boolean;
};

export abstract class GridBase<
	P extends GridBaseProps<C>,
	C extends GridBase<any, any>,
> extends Component<P, C, HTMLDivElement> {
	protected [DATA]: GridBaseData = {
		data: [],
		fixed: 0,
		sorted: null,
		columns: [],
		section: null,
		selected: new Set<unknown>(),
		selectable: false,
		actived: null,
		disableField: null,
		checkcol: {
			style: {
				borderRight: `1px solid ${vars.color.border}`,
			},
			align: 'center',
			width: '4rem',
			fixed: true,
			formatter: (_row, _cell, rowIdx) => {
				const rows = this.sortedData;
				let checked: boolean | 'half';
				if (rowIdx < 0) {
					if (this[DATA].selected.size === 0) {
						checked = false;
					} else if (this[DATA].selected.size === rows.length) {
						checked = true;
					} else {
						checked = 'half';
					}
				} else {
					checked = this[DATA].selected.has(rows[rowIdx]);
				}
				return (
					<Check
						checked={checked === true}
						indeterminate={checked === 'half'}
						ref={(ck) => {
							ck.addEventListener('click', () => {
								if (rowIdx < 0) {
									if (!ck.checked) {
										this[DATA].selected.clear();
									} else {
										this[DATA].selected = new Set(rows);
									}
								} else {
									if (ck.checked) {
										this[DATA].selected.add(rows[rowIdx]);
									} else {
										this[DATA].selected.delete(rows[rowIdx]);
									}
								}
								this.renderSelect();
								this.props.onRowCheck?.call(this as unknown as C, {
									row: rows[rowIdx],
									rowIndex: rowIdx,
									checked: ck.checked,
								});
								this.props.onSelectChange?.call(this as unknown as C, this[DATA].selected);
							});
							ck.addEventListener('pointerdown', (evt) => {
								evt.preventDefault();
								this.el.focus();
								this.setActive(rowIdx);
								// ck.dispatchEvent(new MouseEvent('click'));
							});
						}}
					/>
				);
			},
		},
	};

	// eslint-disable-next-line @typescript-eslint/no-unused-vars
	protected measure(reason: MeasureType): void {}
	protected abstract genStyle(): boolean;
	protected abstract renderBody(): void;
	protected abstract renderSelect(): void;
	protected abstract renderHead(): void;
	protected abstract setActiveRow(rowIdx: number | null, focus: boolean): void;

	protected init() {
		this.el.setAttribute('tabindex', '0');
		this.el.addEventListener('mousedown', (evt) => {
			if (this.activable) {
				const elem = findCell(evt.target as Element | null);
				if (elem) {
					let rid: number | null;
					if (
						elem.rowIdx === this[DATA].actived &&
						this.el === document.activeElement &&
						evt.ctrlKey &&
						evt.button === 0
					) {
						rid = null;
					} else {
						rid = elem.rowIdx;
						if (this.isRowDisableByIndex(rid)) {
							return;
						}
					}
					this.setActiveRow(rid, false);
				}
			}
		});

		this.el.addEventListener('keydown', (evt) => {
			if (!this.activable) return;
			let row = this[DATA].actived;
			const data = this.sortedData;
			if (evt.key === 'ArrowDown') {
				if (row === null) {
					row = 0;
				} else {
					if (row < data.length - 1) {
						row++;
					}
				}
				while (row < data.length) {
					if (this.isRowDisableByIndex(row)) {
						row++;
					} else {
						const srcRow = this[DATA].actived;
						this.setActiveRow(row, true);
						if (row !== srcRow) {
							this.props.onRowActive?.call(this as unknown as C, {
								row: this.activeRow,
								rowIndex: row,
							});
						}
						break;
					}
				}
				evt.preventDefault();
				evt.stopPropagation();
			} else if (evt.key === 'ArrowUp') {
				if (row === null) {
					row = data.length - 1;
				} else {
					if (row > 0) {
						row--;
					}
				}
				while (row >= 0) {
					if (this.isRowDisableByIndex(row)) {
						row--;
					} else {
						const srcRow = this[DATA].actived;
						this.setActiveRow(row, true);
						if (row !== srcRow) {
							this.props.onRowActive?.call(this as unknown as C, {
								row: this.activeRow,
								rowIndex: row,
							});
						}
						break;
					}
				}
				evt.preventDefault();
				evt.stopPropagation();
			}
		});

		this.el.addEventListener('keyup', (evt) => {
			if (evt.key === 'Enter') {
				evt.preventDefault();
				evt.stopPropagation();
			}
		});

		this.el.addEventListener('keypress', (evt) => {
			if (!this.activable) return;
			const idx = this.active;
			if (idx === null) {
				return;
			}
			const row = this.sortedData[idx];
			if (!row) {
				return;
			}
			if (this.isRowDisable(row)) {
				return;
			}
			evt.preventDefault();
			this.props.onRowPress?.call(
				this as unknown as C,
				{
					key: evt.key,
					code: evt.code,
					row,
					rowIndex: idx,
				},
				evt,
			);
		});
		const handleMouseEvent = (
			evt: MouseEvent,
			handleFunc?: (detail: GridCellClickDetail, evt: MouseEvent) => void,
		) => {
			const elem = findCell(evt.target as Element | null);
			if (elem) {
				const row = this.sortedData[elem.rowIdx];
				if (this.isRowDisable(row)) {
					return;
				}
				handleFunc?.call(
					this,
					{
						row,
						rowIndex: elem.rowIdx,
						colIndex: elem.colIdx,
					},
					evt,
				);
			}
		};
		this.el.addEventListener('click', (evt) => {
			handleMouseEvent(evt, this.props.onRowClick);
		});
		this.el.addEventListener('contextmenu', (evt) => {
			handleMouseEvent(evt, this.props.onRowContextMenu);
		});
		this.el.addEventListener('dblclick', (evt) => {
			handleMouseEvent(evt, this.props.onRowDblClick);
		});
		this.setColumnBorder(this.props.columnBorder ?? true)
			.setColumnSize(this.props.columnSize ?? 'auto')
			.setColumns(this.props.columns ?? [])
			.setFixColumns(this.props.fixColumns ?? 0)
			.setHeadLine(this.props.headLine ?? true)
			.setSortType(this.props.sortType ?? 'none')
			.setShowEmpty(this.props.showEmpty ?? true)
			.setSelectable(this.props.selectable ?? false)
			.setActivable(this.props.activable ?? false)
			.setDisableOn(this.props.disableOn ?? null)
			.setSection(this.props.section ?? null)
			.setRows(this.props.rows ?? [])
			.setSelected(this.props.selected ?? [])
			.setActive(this.props.active ?? null);
	}

	protected adjustColumnSize() {
		if (!this.el.isConnected) {
			return this.getColumns();
		}
		const total = this.el.clientWidth;
		// get all setted with
		let logicTotal = 0;
		let unspecified = 0;
		const columns = this.getColumns();
		columns.forEach((c) => {
			let width = size2px(c.width, this.el, false);
			if (width === undefined) {
				c.width = undefined;
				unspecified++;
			} else if (c.fixed) {
				logicTotal += width;
			} else {
				if (width < MIN_WIDTH) {
					width = MIN_WIDTH;
				}
				c.width = width;
				logicTotal += width;
			}
		});
		let unspecifyWidth = MIN_WIDTH;
		if (unspecified > 0) {
			unspecifyWidth = Math.floor((total - logicTotal) / unspecified) - 1;
		}
		if (unspecifyWidth < MIN_WIDTH) {
			unspecifyWidth = MIN_WIDTH;
		}
		logicTotal += unspecifyWidth * unspecified;
		if (this.columnSize === 'scale') {
			// logic with to real with
			columns.forEach((c) => {
				if (c.width === undefined) {
					c.width = (unspecifyWidth / logicTotal) * 100 + '%';
				} else if (!c.fixed) {
					c.width = ((c.width as number) / logicTotal) * 100 + '%';
				}
			});
		} else {
			columns.forEach((c) => {
				if (c.width === undefined) {
					c.width = unspecifyWidth;
				}
			});
		}
		return columns;
	}

	protected get sortedData(): RowType[] {
		if (this[DATA].sorted) {
			return this[DATA].sorted;
		}
		const columns = this.getColumns();
		if (columns.filter((c) => c.orderBy === 'asc' || c.orderBy === 'desc').length === 0) {
			return this[DATA].data;
		}
		this[DATA].sorted = Array.from(this[DATA].data).sort((a, b) => {
			for (const c of columns) {
				if (c.key === undefined) continue;
				if (c.orderBy === 'asc') {
					const aValue = getValue(a, c);
					const bValue = getValue(b, c);
					const result = compare(aValue, bValue, c);
					if (result !== 0) {
						return result;
					}
				} else if (c.orderBy === 'desc') {
					const aValue = getValue(a, c);
					const bValue = getValue(b, c);
					const result = compare(bValue, aValue, c);
					if (result !== 0) {
						return result;
					}
				} else continue;
			}
			return 0;
		});
		return this[DATA].sorted;
	}

	protected getColumns(): Column[] {
		if (this.selectable) {
			return [this[DATA].checkcol, ...this[DATA].columns];
		} else {
			return this[DATA].columns;
		}
	}

	protected isRowDisableByIndex(idx: number): boolean {
		return this.isRowDisable(this.sortedData[idx]);
	}

	protected isRowDisable(row: RowType | undefined | null): boolean {
		return this[DATA].disableField !== null && row !== undefined && row !== null
			? !!(row as any)[this[DATA].disableField]
			: false;
	}

	protected renderSection<T extends HTMLElement>(
		cell: T,
		value: unknown,
		row: RowType,
		rowIdx: number,
		lastCol: Column,
	): T {
		const sec = this[DATA].section as Section;
		cell.className = `${sec.align ?? 'center'} ${lastCol.resizable ? 'rb' : ''}`;
		(cell as any).rowIdx = rowIdx;
		(cell as any).colIdx = 0;
		cell.style.flex = '1';
		if (sec.style) {
			setCSS(cell, sec.style);
		}
		if (typeof sec.formatter === 'function') {
			const contents = sec.formatter(value, rowIdx);
			if (Array.isArray(contents)) {
				cell.append(...contents);
			} else {
				cell.append(contents);
			}
		} else {
			cell.append((value ?? '') + '');
		}
		return cell;
	}

	protected renderCell<T extends HTMLElement>(
		cell: T,
		row: RowType,
		rowIdx: number,
		col: Column,
		colIdx: number,
	): T {
		// const cell = dom.td('c' + colIdx, col.resizable ? 'rb' : '', col.align ?? 'left');
		cell.className = `${'c' + colIdx} ${col.resizable ? 'rb' : ''} ${col.align ?? 'left'} ${colIdx < this.fixColumns ? 'fixed' : ''}`;
		(cell as any).rowIdx = rowIdx;
		(cell as any).colIdx = colIdx;
		if (col.style) {
			setCSS(cell, col.style);
		}
		const value = getValue(row, col);
		if (typeof col.formatter === 'function') {
			const contents = col.formatter(row, value, rowIdx, colIdx);
			if (Array.isArray(contents)) {
				cell.append(...contents);
			} else {
				cell.append(contents);
			}
		} else {
			cell.append((value ?? '') + '');
		}
		return cell;
	}

	protected renderHeadCell<T extends HTMLElement>(cell: T, col: Column, colIdx: number): T {
		const sortIcon = () => {
			let icon: string;
			if (col.orderBy === 'asc') {
				icon = SVGArrowDown;
			} else if (col.orderBy === 'desc') {
				icon = SVGArrowUp;
			} else {
				icon = SVGSort;
			}
			return (
				<span
					class={`sort ${col.orderBy ?? ''}`}
					style={{
						display: 'flex',
						justifyContent: 'center',
						alignItems: 'center',
					}}
				>
					<Icon svg={icon} />
				</span>
			);
		};
		const dragHandler = () => {
			return (
				<div
					class="handle"
					onPointerDown={(evt) => {
						evt.preventDefault();
						evt.stopPropagation();
						const x = evt.clientX;
						const oldWidth = cell.offsetWidth;
						DragMask.show(evt, 'col-resize', (ev) => {
							const moveX = ev.clientX - x;
							col.width = oldWidth + moveX;
							if (col.width < 10) {
								col.width = 10;
							}
							if (this.genStyle()) {
								this.measure('col-resize');
								this.renderBody();
							}
						});
					}}
					onTouchMove={(evt) => {
						evt.preventDefault();
						evt.stopPropagation();
					}}
				/>
			);
		};
		const sortable = col.sortable && this.sortType !== 'none';
		cell.className = `${'c' + colIdx} ${col.resizable ? 'rb' : ''} ${sortable ? 'sort' : ''} ${col.align ?? 'left'} ${colIdx < this.fixColumns ? 'fixed' : ''}`;
		if (col.style) {
			setCSS(cell, col.style);
		}
		if (typeof col.formatter === 'function') {
			const contents = col.formatter({}, col.title, -1, colIdx);
			if (Array.isArray(contents)) {
				cell.append(...contents);
			} else {
				cell.append(contents);
			}
		} else {
			cell.append(
				<div
					ref={(div) => {
						if (sortable) {
							div.addEventListener('click', () => {
								const activedRow = this.activeRow;
								this[DATA].sorted = null;
								const srcOrder = col.orderBy;
								if (col.orderBy === 'none' || !col.orderBy) {
									col.orderBy = 'asc';
								} else if (col.orderBy === 'asc') {
									col.orderBy = 'desc';
								} else {
									col.orderBy = 'none';
								}
								if (
									this.sortType !== 'combine' &&
									(srcOrder === 'none' || srcOrder === undefined) &&
									(col.orderBy === 'asc' || col.orderBy === 'desc')
								) {
									this.getColumns().forEach((c, i) => {
										if (i !== colIdx && c.sortable) {
											c.orderBy = 'none';
										}
									});
								}
								this.renderHead();
								this.genStyle();
								this.measure('sort');
								const orderedData = this.sortedData;
								if (activedRow !== null) {
									this[DATA].actived = orderedData.indexOf(activedRow);
								}
								this.renderBody();
								this.props.onReorder?.call(this as unknown as C, {
									ordered: this.sortedData,
								});
							});
						}
					}}
				>
					<span class={col.align ?? 'left'} style={{ flex: 1 }}>
						{col.title}
					</span>
					{sortable ? sortIcon() : []}
				</div>,
				col.resizable ? dragHandler() : '',
			);
		}
		return cell;
	}

	get columnBorder(): boolean {
		return this.el.hasAttribute('column-border');
	}
	setColumnBorder(value: boolean): this {
		if (value) {
			this.el.setAttribute('column-border', '');
		} else {
			this.el.removeAttribute('column-border');
		}
		return this;
	}

	get columnSize(): ColumnSizeType {
		return (this.el.getAttribute('column-size') ?? 'auto') as ColumnSizeType;
	}
	setColumnSize(value: ColumnSizeType): this {
		const current = this.columnSize;
		value = /^(auto|fixed|scale)$/.test(value) ? value : 'auto';
		if (current === 'scale') {
			this.el.scrollLeft = 0;
		}
		if (current === 'scale' && value === 'fixed') {
			this.el.setAttribute('column-size', 'auto');
			this.renderHead();
			this.el.setAttribute('column-size', 'fixed');
			if (this.genStyle()) {
				this.measure('col-resize');
				this.renderBody();
			}
		} else {
			this.el.setAttribute('column-size', /^(auto|fixed|scale)$/.test(value) ? value : 'auto');
			this.renderHead();
			if (this.genStyle()) {
				this.measure('col-resize');
				this.renderBody();
			}
		}
		return this;
	}

	get columns(): Column[] {
		return this[DATA].columns;
	}
	setColumns(cols: Column[]): this {
		this[DATA].columns = cols;
		this[DATA].sorted = null;
		this.renderHead();
		this.genStyle();
		this.measure('columns');
		this.renderBody();
		return this;
	}

	get headLine(): boolean {
		return this.el.hasAttribute('show-head');
	}
	setHeadLine(value: boolean): this {
		if (value) {
			this.el.setAttribute('show-head', '');
		} else {
			this.el.removeAttribute('show-head');
		}
		this.renderHead();
		if (this.genStyle()) {
			this.measure('headline');
			this.renderBody();
		}
		return this;
	}

	get sortType(): SortType {
		return (this.el.getAttribute('sort') ?? 'none') as SortType;
	}
	setSortType(value: SortType): this {
		this.el.setAttribute('sort', /^(simple|combine|none)$/.test(value) ? value : 'none');
		this[DATA].sorted = null;
		this.setActiveRow(null, false);
		this.renderHead();
		if (this.genStyle()) {
			this.measure('sort');
			this.renderBody();
		}
		return this;
	}

	get selectable(): boolean {
		return this[DATA].selectable;
	}
	setSelectable(value: boolean): this {
		this[DATA].selectable = value;
		if (!value) {
			this[DATA].selected = new Set();
		}
		this.renderHead();
		this.genStyle();
		this.measure('columns');
		this.renderBody();
		return this;
	}

	get selected(): RowType[] {
		return this.sortedData.filter((row) => this[DATA].selected.has(row));
	}
	setSelected(rows: RowType[]): this {
		this[DATA].selected = new Set(rows);
		this.renderHead();
		this.genStyle();
		this.measure('selection');
		this.renderBody();
		return this;
	}

	get activable(): boolean {
		return this.el.hasAttribute('activable');
	}
	setActivable(value: boolean): this {
		if (value) {
			this.el.setAttribute('activable', '');
		} else {
			this.el.removeAttribute('activable');
		}
		if (!value) {
			this[DATA].actived = null;
		}
		if (this[DATA].actived !== null && this.isRowDisableByIndex(this[DATA].actived)) {
			this[DATA].actived = null;
		}
		this.setActiveRow(this[DATA].actived, false);
		return this;
	}

	get active(): number | null {
		return this[DATA].actived;
	}
	setActive(pos: number | null, focus = false): this {
		if (this.activable && (pos === null || (pos >= 0 && pos < this.sortedData.length))) {
			if (pos === null || !this.isRowDisableByIndex(pos)) {
				this.setActiveRow(pos, focus);
			}
		}
		return this;
	}

	get activeRow(): RowType | null {
		if (this[DATA].actived === null) return null;
		return this.sortedData[this[DATA].actived];
	}

	get scrollTop(): number {
		return this.el.scrollTop;
	}
	setScrollTop(value: number) {
		this.el.scrollTop = value;
		this.renderBody();
	}

	get rows(): RowType[] {
		return this[DATA].data;
	}
	setRows(rows: RowType[], refresh = true): this {
		this[DATA].data = rows;
		this[DATA].sorted = null;
		this.setActiveRow(null, false);
		if (refresh) {
			this.measure('rows');
			this.renderBody();
		}
		return this;
	}

	getRow(index: number): RowType {
		return this.sortedData[index];
	}

	getRowIndex(row: RowType, rowCompareFunc?: (r1: RowType, r2: RowType) => boolean): number {
		if (typeof rowCompareFunc === 'function') {
			return this.sortedData.findIndex((r) => {
				return rowCompareFunc(r, row);
			});
		} else {
			return this.sortedData.indexOf(row);
		}
	}

	get showEmpty(): boolean {
		return this.el.hasAttribute('show-empty');
	}
	setShowEmpty(value: boolean): this {
		if (value) {
			this.el.setAttribute('show-empty', '');
		} else {
			this.el.removeAttribute('show-empty');
		}
		if (this.rows.length === 0) {
			this.measure('rows');
			this.renderBody();
		}
		return this;
	}

	get fixColumns(): number {
		return this[DATA].fixed;
	}
	setFixColumns(value: number): this {
		if (value < 0 || isNaN(value)) value = 0;
		this[DATA].fixed = value;
		if (this.genStyle()) {
			this.measure('col-fixed');
			this.renderBody();
		}
		return this;
	}

	get section(): Section | null {
		return this[DATA].section;
	}
	setSection(value: Section | null): this {
		this[DATA].section = value;
		this.measure('rows');
		this.renderBody();
		return this;
	}

	get disableOn(): string | number | null {
		return this[DATA].disableField;
	}
	setDisableOn(value: string | number | null): this {
		this[DATA].disableField = value;
		this.renderBody();
		return this;
	}

	get rowDraggable(): boolean {
		return this.props.rowDraggable ?? false;
	}

	setRowDraggable(value: boolean): this {
		this.props.rowDraggable = value;
		this.refresh('data');
		return this;
	}

	refresh(type: 'complete' | 'data' | 'body' = 'complete'): this {
		if (type === 'body') {
			this.renderBody();
			return this;
		}
		if (type === 'complete') {
			this.renderHead();
			this.genStyle();
			this.measure('refresh');
		} else {
			this.measure('current-page');
		}
		this[DATA].sorted = null;
		this.renderBody();
		return this;
	}

	focus() {
		this.el.focus();
	}

	blur() {
		this.el.blur();
	}

	onRowClick(callback: ((detail: GridCellClickDetail, evt: MouseEvent) => void) | null): this {
		this.props.onRowClick = callback ?? undefined;
		return this;
	}
	onRowDblClick(callback: ((detail: GridCellClickDetail, evt: MouseEvent) => void) | null): this {
		this.props.onRowDblClick = callback ?? undefined;
		return this;
	}
	onRowContextMenu(
		callback: ((detail: GridCellClickDetail, evt: MouseEvent) => void) | null,
	): this {
		this.props.onRowContextMenu = callback ?? undefined;
		return this;
	}
	onRowPress(callback: ((detail: GridRowPressDetail, evt: KeyboardEvent) => void) | null): this {
		this.props.onRowPress = callback ?? undefined;
		return this;
	}
	onRowDragStart(callback: ((detail: GridRowDragDetail, evt: DragEvent) => void) | null): this {
		this.props.onRowDragStart = callback ?? undefined;
		return this;
	}
	onRowDragOver(callback: ((detail: GridRowDragDetail, evt: DragEvent) => void) | null): this {
		this.props.onRowDragOver = callback ?? undefined;
		return this;
	}
	onRowDragEnd(callback: ((detail: GridRowDragDetail, evt: DragEvent) => void) | null): this {
		this.props.onRowDragEnd = callback ?? undefined;
		return this;
	}
	onRowDrop(callback: ((detail: GridRowDragDetail, evt: DragEvent) => void) | null): this {
		this.props.onRowDrop = callback ?? undefined;
		return this;
	}
	onRowDragEnter(callback: ((detail: GridRowDragDetail, evt: DragEvent) => void) | null): this {
		this.props.onRowDragEnter = callback ?? undefined;
		return this;
	}
	onRowDragLeave(callback: ((detail: GridRowDragDetail, evt: DragEvent) => void) | null): this {
		this.props.onRowDragLeave = callback ?? undefined;
		return this;
	}
	onRowDrag(callback: ((detail: GridRowDragDetail, evt: DragEvent) => void) | null): this {
		this.props.onRowDrag = callback ?? undefined;
		return this;
	}
	onSelectChange(callback: ((detail: Set<unknown>) => void) | null): this {
		this.props.onSelectChange = callback ?? undefined;
		return this;
	}
	onReorder(callback: ((detail: GridReorderDetail) => void) | null): this {
		this.props.onReorder = callback ?? undefined;
		return this;
	}
	onRowCheck(cb: ((event: RowCheckDetail) => void) | null): this {
		this.props.onRowCheck = cb ?? undefined;
		return this;
	}
}
